unit uGame.local;

interface

uses uAI.local,

     uTyres.local,
     uEngines.local,
     uChassis.local,
     uTeams.local,
     uDriver.local,
     uTrack.local,

     uRace.local,
     uSeason.local;

type
  TFastStats = record
    DriverID  : integer;
    TeamID    : integer;
    TyreID    : integer;
    EngineID  : integer;
    ChassisID : integer;
    TrackID   : integer;

    DriverInfo  : TDriverItem;
    TeamInfo    : TTeamItem;
    TyreInfo    : TTyreItem;
    EngineInfo  : TEngineItem;
    ChassisInfo : TChassisItem;
    TrackInfo   : TTrackItem;
  end;

  TGame = class( TObject )
  public
    TyreList    : TTyreList;
    EngineList  : TEngineList;
    ChassisList : TChassisList;
    TeamList    : TTeamList;
    DriverList  : TDriverList;
    TrackList   : TTrackList;

    Season    : TSeason;
    Race      : TRace;

    //Settings
    Refueling : boolean;
    NewTyres  : boolean;

    //Fast Stored ID's
    SelectedDriver  : TFastStats;
    PrimaryDriver   : TFastStats;
    SecondaryDriver : TFastStats;

    AIBot           : TAIBot;

    constructor Create();
    destructor Destroy; override;

    function Import(FileName:string):boolean;
    procedure New(FileName:string;nTrackID:integer);
    procedure Tick();

    function Chassis(index:integer):TChassisItem;
    function Tyre(index:integer):TTyreItem;
    function Engine(Index:integer):TEngineItem;
    function Driver(Index:integer):TDriverItem;
    function Team(Index:integer):TTeamItem;
    function Track(Index:integer):TTrackItem;

    function GetStats(nDriverID:integer):TFastStats;

    procedure AddPoints();
    function ShowFastestLap():string;

    //Race
    procedure ToGrid(nSession:string);
    procedure SetPractice1();
    procedure SetPractice2();
    procedure SetPractice3();
    procedure SetQualify();
    procedure SetRace();
    procedure SetPodium();
    procedure CheckSession();

    function GetLapTime():real;
    procedure DoLap();

    procedure PostLap();
    procedure SortGrid(); //Shell Sort

    procedure CheckWeather();
  end;

var
  Game : TGame;

implementation

uses SysUtils, uSplitter.common, uFuncs.common, uDebug.common, uSecondsTotime.common,

     uDefs.local, uFuncs.local,

     Math;

constructor TGame.Create();
begin
  inherited Create;

  FreeAndNil( TyreList   );
  FreeAndNil( EngineList );
  FreeAndNil( ChassisList );
  FreeAndNil( TeamList   );
  FreeAndNil( DriverList );
  FreeAndNil( TrackList  );

  TyreList    := TTyreList.Create;
  EngineList  := TEngineList.Create;
  ChassisList := TChassisList.Create;
  TeamList    := TTeamList.Create;
  DriverList  := TDriverList.Create;
  TrackList   := TTrackList.Create;
end;
destructor TGame.Destroy;
begin
  FreeAndNil( TyreList   );
  FreeAndNil( EngineList );
  FreeAndNil( ChassisList );
  FreeAndNil( TeamList   );
  FreeAndNil( DriverList );
  FreeAndNil( TrackList  );

  FreeAndNil( Season );
  FreeAndNil( Race );

  inherited Destroy;
end;
function TGame.Import(FileName:string):boolean;
var Splitter : TSplitter;
begin
  result := FALSE;

  FileName := GetEXEPath + FileName;
  Splitter := TSplitter.Create;

  if ( Splitter.LoadFile( FileName ) ) then
  begin
    result := TRUE;

    with Splitter do
    while GetNextLine() do
    begin
//      DevConsole.Msg(ES_DEBUG, '('+IntToStr(Splitter.Words.Count)+') # '+NextLine);

      if (To_Str(0) = 'TYRE'    ) then TyreList.Add   ( To_Int(1), To_Str(2), To_Int(3), To_Int(4), To_Int(5)) else
      if (To_Str(0) = 'ENGINE'  ) then EngineList.Add ( To_Int(1), To_Str(2), To_Int(3), To_Int(4)) else
      if (To_Str(0) = 'CHASSIS' ) then ChassisList.Add( To_Int(1), To_Str(2), To_Int(3)) else
      if (To_Str(0) = 'TEAM'    ) then TeamList.Add   ( To_Int(1), To_Str(2), To_Int(3), To_Int(4)) else
      if (To_Str(0) = 'DRIVER'  ) then DriverList.Add ( To_Int(1), To_Str(2), To_Str(3), To_Int(4), To_Int(5), To_Int(6), To_Int(7) );
      if (To_Str(0) = 'TRACK'   ) then TrackList.Add  ( To_Int(1), To_Str(2), To_Str(3), To_Int(4), To_Int(5), To_Float(6), To_Int(7), To_Int(8) );
    end;
  end;
end;
procedure TGame.New(FileName:string;nTrackID:integer);
begin
  FreeAndNil( Season );
  FreeAndNil( Race );

  if (Import( FileName )) then
  begin
    Season    := TSeason.Create();
    Season.New();

    Race      := TRace.Create(DriverList.Count);

    SetPractice1();

    Refueling := FALSE;
    NewTyres  := TRUE;

    if (nTrackID >= 0) then
      Race.TrackID := nTrackID
    else
      Race.TrackID    := Random(TrackList.Count);

//    SelectedDriver  := 0;
//    PrimaryDriver   := SelectedDriver;
//    SecondaryDriver := SelectedDriver;
  end;
end;
procedure TGame.Tick();
begin
  if (Race.Lap >= Race.TotalLaps) then exit;

  Inc( Race.Lap );

  DoLap();
end;

function TGame.Chassis(Index:integer):TChassisItem;
begin
  result := ChassisList.Items[Index];
end;
function TGame.Tyre(Index:integer):TTyreItem;
begin
  result := TyreList.Items[Index];
end;
function TGame.Engine(Index:integer):TEngineItem;
begin
  result := EngineList.Items[Index];
end;
function TGame.Driver(Index:integer):TDriverItem;
begin
  result := DriverList.Items[Index];
end;
function TGame.Team(Index:integer):TTeamItem;
begin
  result := TeamList.Items[Index];
end;
function TGame.Track(Index:integer):TTrackItem;
begin
  result := TrackList.Items[Index];
end;

function TGame.GetStats(nDriverID:integer):TFastStats;
begin
  with result do
  begin
    DriverID    := nDriverID;
    DriverInfo  := Driver( DriverID );

    TeamID      := DriverInfo.TeamID;
    TeamInfo    := Team( TeamID );

    TyreID      := DriverInfo.Tyres;
    TyreInfo    := Tyre( TyreID );

    EngineID    := TeamInfo.EngineID;
    EngineInfo  := Engine( EngineID );

    ChassisID   := TeamInfo.ChassisID;
    ChassisInfo := Chassis( ChassisID );

    TrackID     := Race.TrackID;
    TrackInfo   := Track( TrackID );
  end;
end;

//stuff
procedure TGame.AddPoints();
var G : integer;
begin
  DevConsole.Msg(ES_DEBUG, '[TSeason.AddPoints()]');

  with Race do
  begin
    Inc( Driver( Grid[0] ).Points,  25);
    Inc( Driver( Grid[1] ).Points,  18);
    Inc( Driver( Grid[2] ).Points,  15);
    Inc( Driver( Grid[3] ).Points,  12);
    Inc( Driver( Grid[4] ).Points,  10);
    Inc( Driver( Grid[5] ).Points,  8);
    Inc( Driver( Grid[6] ).Points,  6);
    Inc( Driver( Grid[7] ).Points,  4);
    Inc( Driver( Grid[8] ).Points,  2);
    Inc( Driver( Grid[9] ).Points,  1);


    for G := 0 to High( DriverList.Items ) do
    begin
      Season.DriverStandings[G].Index  := G;
      Season.DriverStandings[G].Points := Game.Driver( G ).Points;
    end;

    with Game do
    begin
      for G := 0 to High( TeamList.Items ) do
        Team( G ).Points := 0;

      for G := 0 to High( DriverList.Items ) do
        with Driver( G ) do
          Inc( Team( TeamID ).Points, Points );
    end;

    for G := 0 to High( TeamList.Items ) do
    begin
      Season.TeamStandings[G].Index  := G;
      Season.TeamStandings[G].Points := Game.Team( G ).Points;
    end;

    with Season do
    begin
      SortStandings(DriverStandings);
      SortStandings(TeamStandings);
    end;
  end;
end;
function TGame.ShowFastestLap():string;
var SecondsToTime : tSecondsToTime;
begin
  SecondsToTime := TSecondsToTime.Create();

  with DriverList.Items[ Race.FastestDriver.ID ] do
    result:= Name+' '+SecondsToTime.Convert( Race.FastestDriver.Time )+' Lap: '+IntToStr(Race.FastestDriver.Lap);

  FreeAndNil( SecondsToTime );
end;

//Race
procedure TGame.ToGrid(nSession:string);
var G, Rand : integer;
begin
//============RACE
  DevConsole.Msg(ES_DEBUG, '[Session] '+nSession);

  with Race do
  begin
    Race.Session := nSession;

//    DevConsole.Msg(ES_DEBUG, '[TGame.RaceReset] Track.ResetGrip');

    Race.Lap := 0;

    Rand     := Random(100);

    if (Rand < Race.Weather) then
      Race.Weather   := 50+Random(Track(Race.TrackID).Weather)//Random(20)*5;
    else
    if (Rand > Race.Weather) then
      Race.Weather   := Random(Track(Race.TrackID).Weather);

    Race.Predicted := Race.Weather;
    Race.Temp      := 30;
  end;

//============TRACK
  with Track(Race.TrackID) do
    Grip := Grip - (40+Random(30)); //Reduces grip from each session, would be better with weather conditions (cold,rain,etc)

//============DRIVERS
  for G := 0 to High( Race.Grid ) do
  begin
    with Driver( Race.Grid[G] ) do
    begin
      NewSession();

      if (Race.Session = SESSION_QUALIFY) then
        FuelStints := ceil( Race.TotalLaps/(3+Random(2)) )
      else
      begin
        FuelStints := 1;
{
        case Random(1000) of
          0..1      : FuelStints := 1;
          2..100    : FuelStints := 2;
          101..500  : FuelStints := 3;
          501..900  : FuelStints := 4;
          else        FuelStints := 5;
        end;

        FuelStints := 2+Random(3);
{}    end;

      AIBot.Think_PitStopStints( Game.DriverList.Items[ Race.Grid[G] ], Race.TotalLaps ); // Would be AI Phase...

      FuelToFill := ceil( Race.TotalLaps / FuelStints )+2;
      TyresToFit := AIBot.Think_Tyres(Game.DriverList.Items[ Race.Grid[G] ], Game.TyreList, Race.Weather);

      Status := [STATUS_PIT] + [STATUS_FUEL] + [STATUS_TYRES];

      with Race do
        PitStop(Lap, TotalLaps, Track(Race.TrackID).FuelPerLap, FALSE); //fills up with initial session fuel

      Status := [STATUS_GRID];
    end;
  end;
end;
procedure TGame.SetPractice1();
begin //This will be eventually used for increasing driver ability for the race, think of car setup time.
  with Race do
  begin
    New();

    TotalLaps := LAPS_PRACTICE1;
  end;

  ToGrid(SESSION_PRACTICE1);
end;
procedure TGame.SetPractice2();
begin //This will be eventually used for increasing driver ability for the race, think of car setup time.
  with Race do
    TotalLaps := LAPS_PRACTICE2;

  ToGrid(SESSION_PRACTICE2);
end;
procedure TGame.SetPractice3();
begin //This will be eventually used for increasing driver ability for the race, think of car setup time.
  with Race do
    TotalLaps := LAPS_PRACTICE3;

  ToGrid(SESSION_PRACTICE3);
end;
procedure TGame.SetQualify();
begin
  with Race do
    TotalLaps := 15;

  ToGrid(SESSION_QUALIFY);
end;
procedure TGame.SetRace();
var G : integer;
begin
  //End Qualify and place people in correct positions for grid.
  with Race do
  begin
    TotalLaps := floor(MAX_DISTANCE_ALLOWED / Track(Race.TrackID).Distance);
    if (TotalLaps > MAX_LAPS_ALLOWED) then TotalLaps := MAX_LAPS_ALLOWED;
  end;

  ToGrid(SESSION_RACE);

  for G := 0 to High( DriverList.Items ) do
  begin
    Driver(Race.Grid[G]).TotalTime := G * 0.4;
    Driver(Race.Grid[G]).TrackPosition := G;
    Driver(Race.Grid[G]).PreviousTrackPosition := G;
  end;
end;
procedure TGame.SetPodium();
begin
  Race.Session := SESSION_PODIUM;

  AddPoints();
end;
procedure TGame.CheckSession();
begin
//  DevConsole.Msg(ES_DEBUG, '[TGame.CheckSession]');

  with Race do
//========
  if (Session = SESSION_PRACTICE1)and(Lap >= TotalLaps) then
//    SetQualify()
    SetPractice2()
  else
  if (Session = SESSION_PRACTICE2)and(Lap >= TotalLaps) then
    SetPractice3()
  else
  if (Session = SESSION_PRACTICE3)and(Lap >= TotalLaps) then
    SetQualify()
  else
  if (Session = SESSION_QUALIFY)and(Lap >= TotalLaps) then
    SetRace()
  else
  if (Session = SESSION_RACE)and(Lap >= TotalLaps) then
    SetPodium()
  else
  if (Session = SESSION_PODIUM)and(Lap >= TotalLaps) then
  begin
    Inc( Race.TrackID );

    if (TrackID > High( TrackList.Items )) then
      TrackID := 0;

    SetPractice1();
  end;
end;

function TGame.GetLapTime():real;
var TrueGrip, TyreOpZone : integer;
begin
//  DevConsole.Msg(ES_DEBUG, '[TGame.GetLapTime()]');

  //Calc Max Speed
  with SelectedDriver do
  begin
//=========== Calcs the Current Tyre Operational Zone
    if (TyreInfo.TOZ > Race.Weather) then
      TyreOpZone := TyreInfo.TOZ - Race.Weather
    else
      TyreOpZone := Race.Weather - TyreInfo.TOZ;

    if (TyreOpZone < 1) then TyreOpZone := 1
    else if (TyreOpZone > MAX_WEATHER) then TyreOpZone := MAX_WEATHER;
//===================

//=========== Calcs Current Grip Level
    with TyreInfo do
      TrueGrip := Round( DryGrip - (DryGrip * (TyreOpZone*0.01)));

    if (TrueGrip < 0) then TrueGrip := 0
    else if (TrueGrip > 100) then TrueGrip := 100;
//===================

    Driver(DriverID).Velocity := ((EngineInfo.MaxKPH/5)*ToPercentage(Track(Race.TrackID).Aero)) + //This will need to be looked at to give fast cars better speed at faster tracks
                                 (20*ToPercentage(ChassisInfo.Aero)) +
                                 (10*ToPercentage(DriverInfo.SkillSpeed)) +
                                 (10*ToPercentage(DriverInfo.SkillFocus)) +

                                 (105*ToPercentage(TrueGrip)) +
                                 (20*(DriverInfo.TyreHealth/100)) +
                                 (15*(Track(Race.TrackID).Grip/100));
  end;

  // Calc time taken to travel circuit with above speed
  result := (Track(Race.TrackID).Distance / ( SelectedDriver.DriverInfo.Velocity / 3600 ));
end;
procedure TGame.DoLap();
var G : integer; nPitStopDelay : real;
begin
//  DevConsole.Msg(ES_DEBUG, '[GRID] '+IntToStr( High( Race.Grid ) ));

  for G:= 0 to High( Race.Grid ) do
  begin
    SelectedDriver := GetStats( Race.Grid[ G ] );
    with Driver( SelectedDriver.DriverID ) do
    begin
      GridSlot := G; // Sets to current grid slot.

      Race.RacingEvents( DriverList.Items[SelectedDriver.DriverID] );

      RemoveFuel( Track(Race.TrackID).FuelPerLap ); // Removes a lap of fuel

      DegradeTyre( Track(Race.TrackID).Distance, SelectedDriver.TyreInfo.WearRate ); // Tyre Wear
    end;
  end;

  for G:= 0 to High( Race.Grid ) do
  begin
    SelectedDriver := GetStats( Race.Grid[ G ] );

    Driver( SelectedDriver.DriverID ).GridSlot := G; // Sets to current grid slot.

    AIBot.BotThink( Game.DriverList.Items[ SelectedDriver.DriverID ], Race, TyreList ); // Would be AI Phase...
  end;

  for G:= 0 to High( Race.Grid ) do
  begin
    SelectedDriver := GetStats( Race.Grid[ G ] );
    with Driver( SelectedDriver.DriverID ) do
    begin
      GridSlot := G; // Sets to current grid slot.

      with Race do
        nPitStopDelay := PitStop(Lap, TotalLaps, Track(Race.TrackID).FuelPerLap, TRUE); // Driver checks for fuel levels and pits if low on fuel.

      if (nPitStopDelay > 0) then
        nPitStopDelay := nPitStopDelay + Race.Time_TravelPitLane();

      with Race do
      LapTime  := GetLapTime() +
                  nPitStopDelay +
                  Time_FuelPerLap(Fuel)+
                  Time_RandomEvent(DriverList.Items[SelectedDriver.DriverID]);

      if (Race.Session = SESSION_RACE) then
        LapTime := LapTime +
                   GetDrag();

      FuelLaps := Fuel2Laps(Track(Race.TrackID).FuelPerLap);
//--------------
//      DevConsole.Msg(ES_DEBUG, '[TRace.DoLap] FastDriver');

      with Race do
      if (FastestDriver.ID < 0) then
        FastestDriver.SetTime(DriverID, Lap, LapTime) //Sets Fastest Lap
      else
      if (LapTime < Driver( FastestDriver.ID ).FastestLap.Time) then
        FastestDriver.SetTime(SelectedDriver.DriverID, Lap, LapTime); //Sets Fastest Lap

//      DevConsole.Msg(ES_DEBUG, '[TRace.DoLap] LapDiff');

      if (LastLapTime > 0) then
        LapTimeDiff := LapTime - LastLapTime;

      LastLapTime := LapTime;

//      DevConsole.Msg(ES_DEBUG, '[TRace.DoLap] FastLap');

      with Race do
      if (FastestLap.Time <= 0)or(LapTime <= FastestLap.Time) then
        FastestLap.SetTime(SelectedDriver.DriverID, Lap, LapTime );
//--------------

      if (STATUS_RACING in Status) and (Track(Race.TrackID).Grip < 100) then
        with Track(Race.TrackID) do
          Grip := Grip + ((100-Track(Race.TrackID).Grip)*(random(200)/10000));

    end;
  end;

  for G := 0 to High( Race.Grid ) do
  begin
    with Driver( Race.Grid[G] ) do
      TotalTime := TotalTime + LapTime;
  end;

  if (Race.Lap > 1) then
    CheckWeather();

  SortGrid();

  PostLap();
end;
procedure TGame.PostLap();
var D : integer;
begin
  for D := 0 to Game.DriverList.Count-1 do
  begin
    with Driver( Race.Grid[D] ) do
    begin
      PreviousTrackPosition := TrackPosition;
      TrackPosition := D;

      if (D > 0) then
      begin
        if (Race.Session <> SESSION_RACE) then
        begin
          TimeToNextDriver  := FastestLap.Time - Driver( Race.Grid[D-1] ).FastestLap.Time;
          TimeToLeader      := FastestLap.Time - Driver( Race.Grid[0] ).FastestLap.Time;
          LeaderLapTimeDiff := LapTime - Driver( Race.Grid[0] ).LapTime;
        end
        else
        begin
          TimeToNextDriver  := TotalTime - Driver( Race.Grid[D-1] ).TotalTime;
          TimeToLeader      := TotalTime - Driver( Race.Grid[0] ).TotalTime;
          LeaderLapTimeDiff := LapTime - Driver( Race.Grid[0] ).LapTime;
        end;
      end
      else
      begin
        TimeToNextDriver  := 0;
        TimeToLeader      := 0;
        LeaderLapTimeDiff := 0;
      end;
    end;
  end;
end;
procedure TGame.SortGrid(); //Shell Sort
var
  ResultID1 : integer;
  ResultID2 : integer;

  Value1,Value2 : real;

  Iteration : Integer;
  Distance  : integer;

  procedure Sort(Distance : Integer);
  var X, Y : Integer;
  begin   {Sort}
    Iteration := 0;
    for Y := Distance + 1 to Length( Race.Grid ) do
    begin  {For}
      X := Y - Distance;
      while X > 0 Do
      begin   {While}
        ResultID1 := Race.Grid[X-1+Distance];
        ResultID2 := Race.Grid[X-1         ];

        if (Race.Session <> SESSION_RACE) then
        begin
          Value1 := Driver( ResultID1 ).FastestLap.Time;
          Value2 := Driver( ResultID2 ).FastestLap.Time;
        end
        else
        begin
          Value1 := Driver( ResultID1 ).TotalTime;
          Value2 := Driver( ResultID2 ).TotalTime;
        end;

        if (Value1 < Value2) then //< - Asscending
        begin
          Race.Grid[X-1+Distance] := ResultID2; //reverses the input
          Race.Grid[X-1         ] := ResultID1;

          Dec(X, Distance);
          Inc(Iteration)
        end     {if}
        else
          X := 0;
      end;    {While}
    end    {For}
  end;    {Sort}

begin   {ShellSort}
  Distance := Length(Race.Grid) div 2;
  while Distance > 0 do
  begin   {While}
    Sort (Distance);
    Distance := Distance div 2
  end;    {While}
end;    {ShellSort}

procedure TGame.CheckWeather();
var Pivot,Rand : integer;
begin
  Pivot := Random(100);

  if (Race.Predicted = Race.Weather)and(random(200) < Track(Race.TrackID).Weather) then
  begin
    case Random(100) of
      0..50   : rand := 10;
      51..80  : rand := 20;
      81..90  : rand := 40;
      else      rand := Random(20)*5;
    end;

    if (Pivot > 50) then
      Race.Predicted := Race.Predicted + rand
    else
      Race.Predicted := Race.Predicted - rand;
  end;

  if (Race.Predicted < Race.Weather) then
    Dec( Race.Weather, 1)
  else
  if (Race.Predicted > Race.Weather) then
    Inc( Race.Weather, 1);

//Weather
  if (Race.Weather > MAX_WEATHER) then
    Race.Weather := MAX_WEATHER
  else
  if (Race.Weather < 0) then
    Race.Weather := 0;

  if (Race.Predicted > MAX_WEATHER) then
    Race.Predicted := MAX_WEATHER
  else
  if (Race.Predicted < 0) then
    Race.Predicted := 0;
end;

end.
